﻿#region << 版 本 注 释 >>
/*----------------------------------------------------------------
 * 版权所有 (c) 2022 北京超维景生物科技有限公司 保留所有权利。
 * 
 * 创建者：huangyang
 * 电子邮箱：huangyang@tvscope.cn
 * 创建时间：2023/3/13 17:25:43
 * 版本：V1.0.0
 * 描述：
 *
 * ----------------------------------------------------------------
 * 修改人：
 * 时间：
 * 修改说明：
 *
 * 版本：V1.0.1
 *----------------------------------------------------------------*/
#endregion << 版 本 注 释 >>

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using ImageK.Gui;
using ImageK.Measure;
using ImageK.Plugin.Frame;
using ImageK.Process;

namespace ImageK.Plugin
{
	/** This plugin implements the Process/Binary/Make Binary 
	and Convert to Mask commands. */
	public class Thresholder : Measurements, PlugIn
	{
	
	    public static string[] methods = AutoThresholder.getMethods();
	    public static string[] backgrounds = new string[] {"Default", "Dark", "Light"};
        private double minThreshold;
        private double maxThreshold;
        private bool _autoThreshold;
        private bool showLegacyDialog = true;
        private static bool fill1 = true;
        private static bool fill2 = true;
        private static bool useBW = true;
        private bool useLocal = true;
        private bool listThresholds;
        private bool createStack;
        private bool convertToMask;
        private string method = methods[0];
        private string background = backgrounds[0];
        private static bool staticUseLocal = true;
        private static bool staticListThresholds;
        private static bool staticCreateStack;
        private static string staticMethod = methods[0];
        private static string staticBackground = backgrounds[0];
        private ImagePlus imp;
        private ArrayList choices;

        public void run(string arg)
        {
	        convertToMask = arg.Equals("mask");
	        if (arg.Equals("skip") || convertToMask)
		        showLegacyDialog = false;
	        ImagePlus imp = IJ.getImage();
	        if (imp.getStackSize() == 1)
	        {
		        Undo.setup(Undo.TRANSFORM, imp);
		        applyThreshold(imp, false);
	        }
	        else
		        convertStack(imp);
	        IJ.showProgress(1.0);
        }

        void convertStack(ImagePlus imp)
        {
	        showLegacyDialog = false;
	        bool thresholdSet = imp.isThreshold();
	        this.imp = imp;
	        if (!IJ.isMacro())
	        {
		        method = staticMethod;
		        background = staticBackground;
		        useLocal = staticUseLocal;
		        listThresholds = staticListThresholds;
		        createStack = staticCreateStack;
		        if (!thresholdSet)
			        updateThreshold(imp);
	        }
	        else
	        {
		        string macroOptions = Macro.GetOptions();
		        if (macroOptions != null && macroOptions.IndexOf("slice ") != -1)
			        Macro.setOptions(macroOptions.Replace("slice ", "only "));
		        useLocal = false;
	        }
	        bool saveBlackBackground = Prefs.blackBackground;
	        bool oneSlice = false;
	        GenericDialog gd = new GenericDialog("Convert Stack to Binary");
	        gd.addChoice("Method:", methods, method);
	        gd.addChoice("Background:", backgrounds, background);
	        gd.addCheckbox("Calculate threshold for each image", useLocal);
	        gd.addCheckbox("Only convert current image", oneSlice);
	        gd.addCheckbox("Black background (of binary masks)", Prefs.blackBackground);
	        gd.addCheckbox("List thresholds", listThresholds);
	        gd.addCheckbox("Create new stack", createStack);
	        choices = gd.getChoices();
	        if (choices != null)
	        {
				//todo:
		        // ((Choice)choices.elementAt(0)).addItemListener(this);
		        // ((Choice)choices.elementAt(1)).addItemListener(this);
	        }
	        gd.showDialog();
	        if (gd.wasCanceled())
		        return;
	        this.imp = null;
	        method = gd.getNextChoice();
	        background = gd.getNextChoice();
	        useLocal = gd.getNextBoolean();
	        oneSlice = gd.getNextBoolean();
	        Prefs.blackBackground = gd.getNextBoolean();
	        listThresholds = gd.getNextBoolean();
	        createStack = gd.getNextBoolean();
	        if (imp.getStack().isVirtual())
	        {
		        oneSlice = false;
		        createStack = true;
	        }
	        if (!IJ.isMacro())
	        {
		        staticMethod = method;
		        staticBackground = background;
		        staticUseLocal = useLocal;
		        staticListThresholds = listThresholds;
		        staticCreateStack = createStack;
	        }
	        if (oneSlice)
	        {
		        useLocal = false;
		        listThresholds = false;
		        createStack = false;
		        if (oneSlice && imp.getBitDepth() != 8)
		        {
			        IJ.error("Thresholder", "8-bit stack required to process a single slice.");
			        return;
		        }
	        }
	        Undo.reset();
	        ImageProcessor ip = imp.getProcessor();
	        double minThreshold = ip.getMinThreshold();
	        double maxThreshold = ip.getMaxThreshold();
	        int currentSlice = imp.getCurrentSlice();
	        if (createStack)
	        {
		        imp = imp.duplicate();
		        imp.setTitle(imp.getTitle().Replace("DUP_", "MASK_"));
		        if (minThreshold != ImageProcessor.NO_THRESHOLD)
			        imp.getProcessor().setThreshold(minThreshold, maxThreshold, ImageProcessor.RED_LUT);
		        imp.setSlice(currentSlice);
	        }
	        if (useLocal)
		        convertStackToBinary(imp);
	        else
		        applyThreshold(imp, oneSlice);
	        Prefs.blackBackground = saveBlackBackground;
	        if (createStack)
	        {
		        imp.setSlice(1);  //why is this needed
		        imp.Show();
		        imp.setSlice(currentSlice);
	        }
	        else if (thresholdSet)
	        {
		        if (imp.getProcessor().getLutUpdateMode() != ImageProcessor.NO_LUT_UPDATE)
			        imp.getProcessor().resetThreshold();
		        imp.updateAndDraw();
	        }
        }

        private void applyThreshold(ImagePlus imp, bool oneSlice)
        {
	        imp.deleteRoi();
	        ImageProcessor ip = imp.getProcessor();
	        ip.resetBinaryThreshold();  // remove any invisible threshold set by Make Binary or Convert to Mask
	        int type = imp.getType();
	        if (type == ImagePlus.GRAY16 || type == ImagePlus.GRAY32)
	        {
		        applyShortOrFloatThreshold(imp);
		        return;
	        }
	        if (!imp.Lock ()) return;
            double saveMinThreshold = ip.getMinThreshold();
            double saveMaxThreshold = ip.getMaxThreshold();
            _autoThreshold = saveMinThreshold == ImageProcessor.NO_THRESHOLD;

            bool useBlackAndWhite = false;
            bool fill1 = true;
            bool fill2 = true;
            string options = Macro.GetOptions();
            bool modernMacro = options != null && !(options.Contains("thresholded") || options.Contains("remaining"));
            if (_autoThreshold || modernMacro || (IJ.MacroRunning() && options == null))
	            showLegacyDialog = false;
            int fcolor = 255, bcolor = 0;

            if (showLegacyDialog)
            {
	            GenericDialog gd = new GenericDialog("Make Binary");
	            gd.addCheckbox("Thresholded pixels to foreground color", fill1);
	            gd.addCheckbox("Remaining pixels to background color", fill2);
	            gd.addMessage("");
	            gd.addCheckbox("Black foreground, white background", useBW);
	            gd.showDialog();
	            if (gd.wasCanceled())
	            { imp.Unlock(); return; }
	            fill1 = gd.getNextBoolean();
	            fill2 = gd.getNextBoolean();
	            useBW = useBlackAndWhite = gd.getNextBoolean();
	            int savePixel = ip.getPixel(0, 0);
	            if (useBlackAndWhite)
		            ip.setColor(Color.Black);
	            else
		            ip.setColor(ToolBar.getForegroundColor());
	            ip.drawPixel(0, 0);
	            fcolor = ip.getPixel(0, 0);
	            if (useBlackAndWhite)
		            ip.setColor(Color.White);
	            else
		            ip.setColor(ToolBar.getBackgroundColor());
	            ip.drawPixel(0, 0);
	            bcolor = ip.getPixel(0, 0);
	            ip.setColor(ToolBar.getForegroundColor());
	            ip.putPixel(0, 0, savePixel);
            }
            else
	            convertToMask = true;

            if (type == ImagePlus.COLOR_RGB)
            {
	            ImageProcessor ip2 = updateColorThresholdedImage(imp);
	            if (ip2 != null)
	            {
		            imp.setProcessor(ip2);
		            _autoThreshold = false;
		            saveMinThreshold = 255;
		            saveMaxThreshold = 255;
		            type = ImagePlus.GRAY8;
	            }
            }
            if (type != ImagePlus.GRAY8)
	            convertToByte(imp);
            ip = imp.getProcessor();

            if (_autoThreshold)
	            autoThreshold(ip);
            else
            {
				//todo:
	            // if (Recorder.record && !Recorder.scriptMode() && (!IJ.isMacro() || Recorder.recordInMacros))
		           //  Recorder.record("//setThreshold", (int)saveMinThreshold, (int)saveMaxThreshold);
	            minThreshold = saveMinThreshold;
	            maxThreshold = saveMaxThreshold;
            }

            if (convertToMask && ip.isColorLut())
	            ip.setColorModel(ip.getDefaultColorModel());
            ip.resetThreshold();

            if (IJ.debugMode) IJ.log("Thresholder (apply): " + minThreshold + "-" + maxThreshold + " " + fcolor + " " + bcolor + " " + fill1 + " " + fill2);
            int[] lut = new int[256];
            for (int i = 0; i < 256; i++)
            {
	            if (i >= minThreshold && i <= maxThreshold)
		            lut[i] = fill1 ? fcolor : (byte)i;
	            else
	            {
		            lut[i] = fill2 ? bcolor : (byte)i;
	            }
            }
            if (imp.getStackSize() > 1 && !oneSlice)
	            new StackProcessor(imp.getStack(), ip).applyTable(lut);
            else
	            ip.applyTable(lut);
            if (convertToMask && !oneSlice)
            {
	            bool invertedLut = imp.isInvertedLut();
	            if ((invertedLut && Prefs.blackBackground) || (!invertedLut && !Prefs.blackBackground))
	            {
		            ip.invertLut();
					//todo:
		            // if (!IJ.isMacro() && ThresholdAdjuster.isDarkBackground() && !invertedLut && !Prefs.blackBackground)
			           //  IJ.log("\"Black background\" not set in Process>Binary>Options; inverting LUT");
		            if (IJ.debugMode) IJ.log("Thresholder (inverting lut)");
	            }
            }
            if (fill1 && fill2 && ((fcolor == 0 && bcolor == 255) || (fcolor == 255 && bcolor == 0)))
            {
	            imp.getProcessor().setThreshold(fcolor, fcolor, ImageProcessor.NO_LUT_UPDATE);
	            if (IJ.debugMode) IJ.log("Thresholder: " + fcolor + "-" + fcolor + " (" + (Prefs.blackBackground ? "black" : "white") + " background)");
            }
            imp.updateAndRepaintWindow();
            imp.Unlock();
	    }
	
        private ImageProcessor updateColorThresholdedImage(ImagePlus imp)
        {
	        object mask = imp.getProperty("Mask");
	        if (mask == null || !(mask is ByteProcessor))
			        return null;
            ImageProcessor maskIP = (ImageProcessor)mask;
            if (maskIP.getWidth() != imp.getWidth() || maskIP.getHeight() != imp.getHeight())
	            return null;
            object originalImage = imp.getProperty("OriginalImage");
            if (originalImage != null && (originalImage is ImagePlus)) {
	            ImagePlus imp2 = (ImagePlus)originalImage;
	            if (imp2.getBitDepth() == 24 && imp2.getWidth() == imp.getWidth() && imp2.getHeight() == imp.getHeight())
	            {
		            imp.setProcessor(imp2.getProcessor());
		            Undo.setup(Undo.TRANSFORM, imp);
	            }
            }
            return maskIP;
	    }
	
        private void applyShortOrFloatThreshold(ImagePlus imp)
        {
	        if (!imp.Lock()) return;
            int width = imp.getWidth();
            int height = imp.getHeight();
            int size = width * height;
            bool isFloat = imp.getType() == ImagePlus.GRAY32;
            int currentSlice = imp.getCurrentSlice();
            int nSlices = imp.getStackSize();
            ImageStack stack1 = imp.getStack();
            ImageStack stack2 = new ImageStack(width, height);
            ImageProcessor ip = imp.getProcessor();
            float t1 = (float)ip.getMinThreshold();
            float t2 = (float)ip.getMaxThreshold();
            if (t1 == ImageProcessor.NO_THRESHOLD)
            {
	            double min = ip.getMin();
	            double max = ip.getMax();
	            ip = ip.convertToByte(true);
	            autoThreshold(ip);
	            t1 = (float)(min + (max - min) * (minThreshold / 255.0));
	            t2 = (float)(min + (max - min) * (maxThreshold / 255.0));
            }
            float value;
            ImageProcessor ip1, ip2;
            IJ.ShowStatus("Converting to mask");
            for (int i = 1; i <= nSlices; i++)
            {
	            IJ.showProgress(i, nSlices);
	            String label = stack1.getSliceLabel(i);
	            ip1 = stack1.getProcessor(i);
	            ip2 = new ByteProcessor(width, height);
	            for (int j = 0; j < size; j++)
	            {
		            value = ip1.getf(j);
		            if (value >= t1 && value <= t2)
			            ip2.set(j, 255);
		            else
			            ip2.set(j, 0);
	            }
	            stack2.addSlice(label, ip2);
            }
            imp.setStack(null, stack2);
            ImageStack stack = imp.getStack();
            stack.setColorModel(LookUpTable.createGrayscaleColorModel(!Prefs.blackBackground));
            imp.setStack(null, stack);
            if (imp.isComposite())
            {
				//todo:
	            // CompositeImage ci = (CompositeImage)imp;
	            // ci.setMode(IJ.GRAYSCALE);
	            // ci.resetDisplayRanges();
	            // ci.updateAndDraw();
            }
            imp.getProcessor().setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE);
            if (IJ.debugMode) IJ.log("Thresholder16: 255-255 (" + (Prefs.blackBackground ? "black" : "white") + " background)");
            IJ.ShowStatus("");
            imp.Unlock();
	    }

        void convertStackToBinary(ImagePlus imp)
        {
	        int nSlices = imp.getStackSize();
	        double[] minValues = listThresholds ? new double[nSlices] : null;
	        double[] maxValues = listThresholds ? new double[nSlices] : null;
	        int bitDepth = imp.getBitDepth();
	        if (bitDepth != 8)
	        {
		        IJ.ShowStatus("Converting to byte");
		        ImageStack stack1 = imp.getStack();
		        ImageStack stack2 = new ImageStack(imp.getWidth(), imp.getHeight());
		        for (int i = 1; i <= nSlices; i++)
		        {
			        IJ.showProgress(i, nSlices);
			        String label = stack1.getSliceLabel(i);
			        ImageProcessor ip = stack1.getProcessor(i);
			        ip.resetMinAndMax();
			        if (listThresholds)
			        {
				        minValues[i - 1] = ip.getMin();
				        maxValues[i - 1] = ip.getMax();
			        }
			        stack2.addSlice(label, ip.convertToByte(true));
		        }
		        imp.setStack(null, stack2);
	        }
	        ImageStack stack = imp.getStack();
	        IJ.ShowStatus("Auto-thresholding");
	        if (listThresholds)
		        IJ.log("Thresholding method: " + method);
	        for (int i = 1; i <= nSlices; i++)
	        {
		        IJ.showProgress(i, nSlices);
		        ImageProcessor ip = stack.getProcessor(i);
		        if (method.Equals("Default") && background.Equals("Default"))
			        ip.setAutoThreshold(ImageProcessor.ISODATA2, ImageProcessor.NO_LUT_UPDATE);
		        else
			        ip.setAutoThreshold(method, !background.Equals("Light"), ImageProcessor.NO_LUT_UPDATE);
		        minThreshold = ip.getMinThreshold();
		        maxThreshold = ip.getMaxThreshold();
		        if (listThresholds)
		        {
			        double t1 = minThreshold;
			        double t2 = maxThreshold;
			        if (bitDepth != 8)
			        {
				        t1 = minValues[i - 1] + (t1 / 255.0) * (maxValues[i - 1] - minValues[i - 1]);
				        t2 = minValues[i - 1] + (t2 / 255.0) * (maxValues[i - 1] - minValues[i - 1]);
			        }
			        int digits = bitDepth == 32 ? 2 : 0;
			        IJ.log("  " + i + ": " + IJ.d2s(t1, digits) + "-" + IJ.d2s(t2, digits));
		        }
		        int[] lut = new int[256];
		        for (int j = 0; j < 256; j++)
		        {
			        if (j >= minThreshold && j <= maxThreshold)
				        lut[j] = (byte)255;
			        else
				        lut[j] = 0;
		        }
		        ip.applyTable(lut);
	        }
	        stack.setColorModel(LookUpTable.createGrayscaleColorModel(!Prefs.blackBackground));
	        imp.setStack(null, stack);
	        imp.getProcessor().setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE);
	        if (imp.isComposite())
	        {
		        // CompositeImage ci = (CompositeImage)imp;
		        // ci.setMode(IJ.GRAYSCALE);
		        // ci.resetDisplayRanges();
		        // ci.updateAndDraw();
	        }
	        IJ.ShowStatus("");
        }

        void convertToByte(ImagePlus imp)
        {
	        ImageProcessor ip;
	        int currentSlice = imp.getCurrentSlice();
	        ImageStack stack1 = imp.getStack();
	        ImageStack stack2 = imp.createEmptyStack();
	        int nSlices = imp.getStackSize();
	        String label;
	        for (int i = 1; i <= nSlices; i++)
	        {
		        label = stack1.getSliceLabel(i);
		        ip = stack1.getProcessor(i);
		        ip.setMinAndMax(0, 255);
		        stack2.addSlice(label, ip.convertToByte(true));
	        }
	        imp.setStack(null, stack2);
	        imp.setSlice(currentSlice);
	        imp.setCalibration(imp.getCalibration()); //update calibration
        }

        /** Returns an 8-bit binary (0 and 255) threshold mask
         * that has the same dimensions as this image.
         * @see ij.process.ImageProcessor#createMask
         * @see ij.ImagePlus#createThresholdMask
         * @see ij.ImagePlus#createRoiMask
        */
        public static ByteProcessor createMask(ImagePlus imp)
        {
	        ImageProcessor ip = imp.getProcessor();
	        if (ip is ColorProcessor)
			        throw new ArgumentException("Non-RGB image requires");
            if (ip.getMinThreshold() == ImageProcessor.NO_THRESHOLD)
	            throw new ArgumentException("Image must be thresholded");
            return ip.createMask();
	    }
	
        void autoThreshold(ImageProcessor ip)
        {
	        ip.setAutoThreshold(ImageProcessor.ISODATA2, ImageProcessor.NO_LUT_UPDATE);
	        minThreshold = ip.getMinThreshold();
	        maxThreshold = ip.getMaxThreshold();
	        if (IJ.debugMode) IJ.log("Thresholder (auto): " + minThreshold + "-" + maxThreshold);
        }

        public static void setMethod(String method)
        {
	        staticMethod = method;
        }

        public static void setBackground(String background)
        {
	        staticBackground = background;
        }

        //todo:
        // public void itemStateChanged(ItemEvent e)
        // {
	       //  if (imp == null)
		      //   return;
	       //  Choice choice = (Choice)e.getSource();
	       //  if (choice == choices.elementAt(0))
		      //   method = choice.getSelectedItem();
	       //  else
		      //   background = choice.getSelectedItem();
	       //  updateThreshold(imp);
        // }

        private void updateThreshold(ImagePlus imp)
        {
	        ImageProcessor ip = imp.getProcessor();
	        if (method.Equals("Default") && background.Equals("Default"))
		        ip.setAutoThreshold(ImageProcessor.ISODATA2, ImageProcessor.RED_LUT);
	        else
		        ip.setAutoThreshold(method, !background.Equals("Light"), ImageProcessor.RED_LUT);
	        imp.updateAndDraw();
        }

}

}
